/* CS32 FINAL PROJECT */

package GameEngine;

import ClientGraphics.GraphicsMain;
import GameEngine.GameConstants.Action;
import GameEngine.GameConstants.Graphic;
import GameEngine.GameConstants.Levels;
import GameEngine.GameConstants.Special;
import Graphics.MainGui;
import Objects.*;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.swing.JFrame;
import javax.swing.JPanel;
/**
 * This class will be extended by ClientUser and LocalUser, but both will
 * have the same functionality and relation as an object in a Game.
 * A user extends thread, because we will need the user to read button presses
 * at all times, and also send the user gamedata every iteration simultaneously.
 * As the thread reads button presses, it should parse these and send them as
 * user actions to the GameServer...but lets make sure to make this threadsafe.
 * @author crdunwel
 */
public class User
{
    // INSTANCE VARIABLES
    private Special type;                   // the special power for the player (found in GameConstants under Special enum)
    public GameServer inGame;               // reference to the server 
    public Socket socket;                   // client's socket to connect to the server
    public String username;                 // username for client
    public UserThread userThread;           // thread that reads information from server to render graphically on screen
    public boolean serverReady;             // boolean to indicate that the server has started the game and client should begin reading objects to render
    public DataOutputStream out;            // stream to write action to server
    public short UID;                       // the UID the server assigns to the client
    public byte levelname;                  // levelname as a byte corresponding to enum in GameConstants
    public boolean victoryCondition;
    public MainGui guiReference;            // used to return to GUI after a victory
    public String addrname;
    public int port;
    public JFrame mainFrame;
    public JPanel gamePanel;

    /**
     * CONSTRUCTOR
     * User
     *
     * This class handles IO with the server by writing user actions to a bytestream.
     * Once an action is written, the server must update the state of the level and
     * send the updated state back to the User.
     *
     * @param name      the username for the player
     * @param port      the port the player is connected to
     * @param addrname  the address name for the player to connect to
     * @param s         the special the player decides
     */
    public User(JFrame frame, String name, int p, MainGui guiMain)
    {
       mainFrame = frame;
       guiReference = guiMain;
       addrname = name;
       port = p;
       socket = createSocket(addrname, port);
       if (socket != null)
       {
            try
            {
                out = new DataOutputStream(socket.getOutputStream());
            }
            catch (IOException e1)
            {
                System.out.println("failed to create dataoutputstream");
            }
       }
    }
    
    //************GETTERS AND SETTERS**********************//
    /**
     * This method sets the username for the client
     * @param name      username for the client
     */
    public void setUserName(String name)
    {
        username = name;
    }

    /**
     * This method sets the type for the client
     * @param spec      type for client
     */
    public void setType(Special spec)
    {
        type = spec;
    }

    /**
     * This method returns the client's socket
     * @return
     */
    public Socket getSocket()
    {
        return socket;
    }

        // gets graphics panel
       public JPanel getPanel() {
             return gamePanel;
         }

    public void reconnect()
    {
        socket = createSocket(addrname, port);
    }

    /**
     * This method begins the user's read thread.  It is called after the user pressed the READY button
     */
    public void initializeUserThread()
    {
        userThread = new UserThread();
        sendUserType();
        userThread.start();
    }

    /**
     * This method takes an address and a port and outputs a Socket to connect to this address/port.
     *
     * @param addrname      the address name for the player to connect to
     * @param port          the port for the player to connect to
     * @return Socket       the Socket connected to addrname on port
     */
    public Socket createSocket(String addrname, int port)
    {
        try
        {
            InetAddress addr = InetAddress.getByName(addrname);
            Socket sock = new Socket(addr, port);
            return sock;
        }
        catch (IOException e1)
        {
            return null;        
        }
    }

    public void sendUserType()
    {
        try
        {
            out.writeByte((byte) type.ordinal());
            System.out.println("Client type: " + type.ordinal());
        }
        catch (IOException e1)
        {
            System.out.println("error writing byte: " + type.ordinal());
        }
    }

    public void writeAction(GameConstants.Action act)
    {
        try
        {
            if (!victoryCondition)
            {
                out.writeByte((byte) act.ordinal());
            }
        }
        catch (IOException e2)
        {

        }
    }

    public void victoryResponse()
    {
        try
        {
            out.writeByte(-2);
            socket.close();
        }
        catch (IOException e2)
        {

        }
    }


    /**
     * This private class handles IO with the Server via a thread.
     */
    private class UserThread extends Thread
    {
        // INSTANCE VARIABLES
        private DataInputStream in;                      // handles input from the server
        public HashMap<Short, BasicObject> objects;      // Keeps track of objects to render to client's screen

        /**
         * CONSTRUCTOR
         * UserThread
         *
         * The UserThread continuously reads for input from server
         */
        public UserThread()
         {
            try
            {
                in = new DataInputStream(socket.getInputStream());
                //objects = new ArrayList<BasicObject>();
                objects = new HashMap<Short, BasicObject>();
            }
            catch (IOException e1)
            {
                // TODO Error message
            }
         }

        /**
         * This method reads the objects from the server, constructs a BasicObject, and adds this object
         * to a list.
         */
        public void readAndSetObjects()
        {
           try
           {
               while (true)
               {
                   // READ OBJECT
                   short idnum = in.readShort();

                   // Victory Conditions
                   if (idnum == -7)
                   {
                       victoryCondition = true;
                       break;
                   }
                   else
                   {
                        // read information from server
                        byte graph = in.readByte();
                        float dirX = in.readFloat();
                        float dirY = in.readFloat();
                        short cenX = in.readShort();
                        short cenY = in.readShort();
                        short sizeX = in.readShort();
                        short sizeY = in.readShort();
                        boolean exists = in.readBoolean();
                        // generate basic object info
                        Vector center = new Vector(cenX,cenY);
                        Vector direction = new Vector(dirX,dirY);
                        Vector size = new Vector(sizeX,sizeY);
                        Graphic graphic = GameConstants.parseByteToGraphic(graph);
                         // checks if object already exists for client
                         if (objects.containsKey(idnum))
                         {
                            // update values
                            objects.get(idnum).setCenter(center);
                            objects.get(idnum).setDir(direction);
                            objects.get(idnum).setSize(size);
                            objects.get(idnum).setGraph(graphic);
                            objects.get(idnum).setExists(exists);
                         }
                        // creates and adds objects to map if it doesn't exist
                        else
                        {
                            BasicObject toAdd = new BasicObject(direction,center,size,graphic);
                            toAdd.setId(idnum);
                            toAdd.setExists(exists);
                            objects.put(idnum, toAdd);
                        }

                        // checks if end of object list
                        if (in.readBoolean() == true)
                        {
                            break;
                        }
                   }
               }
           }
           catch (IOException e1)
           {
                //TODO make error
           }
        }

        public List<BasicObject> mapToList()
        {
            Set<Short> keys = objects.keySet();
            List<BasicObject> toReturn = new LinkedList<BasicObject>();
            for (Short id : keys)
            {
                toReturn.add(objects.get(id));
            }
            return toReturn;
        }

         @Override
         /**
          * This method continuously checks if an action has been committed and if so sends the action
          * to the server.  Regardless if an action has been committed, the thread reads the updated
          * state of the level from the server.
          */
         public void run()
         {
            try
            {
                // reads UID assigned to client by server
                UID = in.readShort();
                // reads level from the server
                levelname = in.readByte();
                // converts the byte to a Levels enum
                Levels lEnum = GameConstants.parseByteToLevels(levelname);
                System.out.println("level byte: " + levelname);
                System.out.println(lEnum.toString());
                int levelSizeX = in.readInt();
                int levelSizeY = in.readInt();
                // sets up GUI to display graphics to user
                GraphicsMain gMain = new GraphicsMain(lEnum, new Dimension(mainFrame.getWidth(), mainFrame.getHeight()), levelSizeX, levelSizeY);
                System.out.println("Client UID: " + UID);
                gMain.setUID(UID);
                // to allow for system testing without gui
                if (guiReference == null) {
                    gamePanel = new JPanel();
                }
                // otherwise normal mode
                else {
                    gamePanel = guiReference.getGamePanel();
                }
                gMain.setGraphicsPanel(gamePanel);
                gMain.drawWaitScreen();
                // check if server is ready to begin game
                boolean serverStatus = in.readBoolean();
                serverReady = serverStatus;
                // begins to display graphics sent to user from server
                gMain.initialize();
                while (serverReady)
                {
                    // read objects sent by server
                    readAndSetObjects();
                    // checks to see if victory condition is met
                    if (victoryCondition)
                    {
                        victoryResponse();
                        System.out.println("User Victory Case");
                        gMain.stopRendering();
                        mainFrame.remove(gMain.getGraphicsPanel());
                        CardLayout cl = (CardLayout)(guiReference.getContainPanel().getLayout());
                        cl.show(guiReference.getContainPanel(), "Main");
                        victoryCondition = false;
                        stop();
                    }
                    // otherwise update the graphics
                    else
                    {
                        gMain.update(mapToList());
                    }
                }
            }
            catch (IOException ex)
            {
                //TODO handle error
            }

         }


      


    }


}
